Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid resetting Group transform by default #9525

Closed
wants to merge 10 commits into from

Conversation

jiayihu
Copy link
Contributor

@jiayihu jiayihu commented Dec 5, 2023

Resetting the transform if the group is temporarily empty is very dangerous and unexpected, especially if FixedLayout is used. It makes sense only for FitContentLayout IMO, where by definition if there is no content then the Group position is unknown.

@jiayihu jiayihu changed the title Avoid resetting transform by default Avoid resetting Group transform by default Dec 5, 2023
Copy link
Contributor

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks
I agree with this PR
Please add a test covering this
And we merge
@asturur take a look

ShaMan123
ShaMan123 previously approved these changes Dec 5, 2023
Copy link
Contributor

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a spy to shouldResetTransform and expect it it return false as well?

@jiayihu
Copy link
Contributor Author

jiayihu commented Dec 6, 2023

I was actually wondering if we should keep the shouldResetTransform method at all. It's clearly made for the FitContentLayout. We could have instead a onAfterLayout hook for the Strategy and do the transform reset there for FitContentLayout. It will be noop for the other layouts and leave more flexibility for other custom logic on after layout.

@ShaMan123
Copy link
Contributor

I am open for that,
Also moving the layout object to the strategy makes sense I believe

@ShaMan123
Copy link
Contributor

ShaMan123 commented Dec 6, 2023

How will we expose something like the following which belongs only to fixed layout ?

setGroupDimensions(dims,origin): void

origin can be a point same as origin in scaling/rotating

@ShaMan123
Copy link
Contributor

Maybe it should be a method on LayoutManager that calls performLayout with a FixedLayout strategy, width, height and origin

@ShaMan123
Copy link
Contributor

Also shouldLayoutClipPath... if we move layoutObjects to the strategy that can go away as well

@asturur
Copy link
Member

asturur commented Dec 7, 2023

So i may agree with this pr or at least i don't disagree.
But i do need to know how you got there.
You must have been doing something for which at this point when the group was empty and you somehow needed to know where the group was, and i would like to know reasons and use case if they are not covered by some work secret. because it helps building api use cases and api understanding.

@jiayihu
Copy link
Contributor Author

jiayihu commented Dec 7, 2023

I've refactored into using onAfterLayout hook instead of shouldResetTransform.

How will we expose something like the following which belongs only to fixed layout ?

That's for a separate PR.

You must have been doing something for which at this point when the group was empty and you somehow needed to know where the group was, and i would like to know reasons and use case if they are not covered by some work secret

Nothing secret. From the moment the Layout Manager has been introduced, we've migrated the groups to use it because it's much more easier to work with it reliably, without surprises with the Group auto-resizing. Then we carefully manually call triggerLayout when necessary. This is a much more predictable behaviour than FitContent.
For our case, where we have a collaborative canvas, changes must be synced across users. During the programmatic Group manipulations, it can happen that a Group is without children during an intermediate state. For instance, we could be temporarily swapping all the children out of a Group, do some transforms and re-put them inside the Group.

A different perspective is realizing that with the Layout Manager, we have a first step towards a layout system, like the CSS one. So just like you can have an empty DIV with a fixed size and position, for whatever reason, we can wish for a Fixed Group, e.g. where to drop other objects as drop area.

Copy link
Contributor

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So let's go for what you suggested in the first place and expose more of the lifecycle on the strategy
onBeforeLayout, what else?

@jiayihu
Copy link
Contributor Author

jiayihu commented Dec 11, 2023

Added onBeforeLayout

@asturur
Copy link
Member

asturur commented Dec 11, 2023

so let's focus one second on this transform reset.
If i have a fixed layout group i can see why you don't want any automatism on transform properties, and that is ok, we don't need to talk about that.

Then we have a fit content.
fit content has to wrap content tightly and that its only real job.
If you create a group with objects and then you scale it and rotate it, it makes sense that when is empty it looses at least the rotation and this because we use it for active selection. Since the active selection when is emptied is not trashed, you don't want to see a new active selection with the rotation of the previous one. If it is skewed the same apply, while scale would probably be visually neutral but given 2 over 3 are unexpected, is ok to reset them.
But left and top?
0 and 0 are not 'unset' are a position like another, why would we care to set them to 0 when the group empties? if a new object gets inserted i would assume the new center of the group takes in account only the new single obejct contained and the old position doesn't influence anything.

@asturur
Copy link
Member

asturur commented Dec 11, 2023

i think this whole reset transform is an active selection issue and we can just have an active selection strategy,
but we don't need to do this in the pr

@ShaMan123
Copy link
Contributor

ShaMan123 commented Dec 11, 2023

0 and 0 are not 'unset' are a position like another, why would we care to set them to 0 when the group empties? if a new object gets inserted i would assume the new center of the group takes in account only the new single obejct contained and the old position doesn't influence anything.

This sounds correct but needs to be tested, I am not sure what happens. Maybe I did that because of correction or something like that that influnced the layout.

i think this whole reset transform is an active selection issue and we can just have an active selection strategy

ActiveSelection is a special entity with a set of expectations and the transform reset is part of it but the discussion is if it is part of how ActiveSelection behaves or how FitContent behaves, a discussion possible thanks to our work on extracting everything to a standalone.

Our question

A group has transform and children.
All objects are removed from it.
A single object is then added to it.
What transform does the group have?

Expected Behavior

The added object should seem unchanged after entering the group.
The group should draw a bbox around the object.

Thoughts

I think that the group lost its transform when it lost the objects.
Since we expect it to draw a bbox around the single object it means it has no rotation. Furthermore, I expect it to draw a bbox that is not aligned to the object's main axis but to the group's parent's main axis so even if the object is rotated the group will bound it and not be affected by its rotation.
After loosing all objects the group lost its size. What scale/skew did it have after loosing the objects/size? scale/skew awithout size are meaningless.
Seems to me this is a special transform action where width/height affect scale/skew.

@ShaMan123
Copy link
Contributor

updated my comment

@asturur
Copy link
Member

asturur commented Dec 11, 2023

ok i think your question is good:

A group has transform and children.
All objects are removed from it.
A single object is then added to it.
What transform does the group have?

I would need to dig deep into 5.x to answer this question correctly, but my answer is that the group should have the transform it had before, because the group is in a developer driven flow so if the developer didn't touch scale, by default the developer should assume that no one touched scale.

Before we refactored stuff the active selection was deleted as soon as it lost all objects and so the issue of resetting the transform wasn't there at all.
Since the activeSlection is supporting a pre built user flow it makes sense that has some built in behaviour.

But in general if you remove the objects from a group, you empty the objects array and that's it. Nothing else should happen to the group. ( fit content will change width/height, fixed layout not, clip-path layout not probably ).

For this PR since now the situation is this one, and this PR is just changing the way we make this prebuilt flow and we exclude the fixed-layout from it, i don't see any issue.

But my gut feeling is that it should be confined to the activeSelection.
We should dig into commits to understand when we added it.

@asturur
Copy link
Member

asturur commented Dec 11, 2023

I can add more saying, that if you think the transform should be reset, just make a new group. if you want the transform to not be lost reuse the same group.

Comment on lines 538 to 540
const manager = new LayoutManager(
reset ? new FitContentLayout() : new FixedLayout()
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this way the test isn't clear anymore.
please instead of true false in test.each, pass directly the strategy manager in the array, something like:

test.each([new FitContentLayout(), new FixedLayout()])('reset target transform %s', (strategyManager) => {

if it doesn't work with the title, just make 2 tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, but slightly differently. Please have a look.

asturur
asturur previously approved these changes Dec 11, 2023
Copy link
Member

@asturur asturur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart a change in tests i m good with this.
Please improve the test

@asturur
Copy link
Member

asturur commented Dec 11, 2023

urgh a circulare dependency

@jiayihu
Copy link
Contributor Author

jiayihu commented Dec 11, 2023

i think this whole reset transform is an active selection issue and we can just have an active selection strategy,

I agree. Regarding whether a FitContentLayout should reset all the transforms or not, I'd say it's undefined behaviour (UB). In TS terms, one could expect a Group with FitContentLayout to have a NonEmptyArray of objects, thus you can't remove all the children. In turn you just use a new Group as you suggested.

While I'm not suggesting to go that far with TS, it helps us reasoning in logical sense. So we can move the reset transform out of FitContentLayout and, then, not worry about what happens if you remove all the children. If a developer goes into UB territory anything is possible and it's up to you. For what we know, we could even technically reach division by zero, since size is zero. And division by zero is UB indeed, e.g.:

Screenshot 2023-12-11 at 23 19 24

@ShaMan123
Copy link
Contributor

This discussion is beneficial.
We have agreed that resetting transform is ActiveSelectionLayout domain for sure.
We can do something else.
We can say that if the active selection looses all it's objects it is dumped (as it was before the active selection ref change) and a new one is created and assigned to canvas and have all this conversation obsolete.
Then we only need to handle event subscription some how, subscribing the new active selection with the old event handlers. Not sure how that can be done gracefully.

Regarding FitContent and group transform... I agree that it is unexpected for group transform to suddenly reset so we should document this part as an expectation and how to handle it.

@ShaMan123
Copy link
Contributor

Then we only need to handle event subscription some how, subscribing the new active selection with the old event handlers. Not sure how that can be done gracefully.

Actually the only need I can think of for the active selection ref is event subscription.
That was why I did it.
I could have instead subscribed to canvas level events and checked if the target is an active selection...
So maybe that is how it should be done to begin with and this complexity I added was indeed wrong.

@ShaMan123
Copy link
Contributor

Regarding the blame.
I added a transform reset on active selection after a bug was reported here

Then in #9152 I moved it to https://github.com/fabricjs/fabric.js/pull/9152/files#diff-dc26515afb0f87d2f6aa640d27b92a85ec523152d3aac8bc294ec59a66546295R262-R266

@ShaMan123
Copy link
Contributor

A few thoughts after reviewing this in depth.

  1. LayoutStrategy#onBeforeLayout should be called after subscription so it can tweak that if it needs
  2. Lifecycle tests must be updated because I have found an infinite loop in a test case
  3. If our decision is that only active selection resets its transform then we could/should move that to the active selection itself and listen to layout:after, then all this PR is obsolete

I have local changes to push but tests are still looping infinately

@jiayihu
Copy link
Contributor Author

jiayihu commented Dec 21, 2023

I have local changes to push but tests are still looping infinately

Have you pulled the latest changes? The only import that has changed is resetObjectTransform, that now I'm importing directly instead from the utils barrel file

@asturur
Copy link
Member

asturur commented Dec 28, 2023

What we want to do with this PR?

@ShaMan123
Copy link
Contributor

As we discussed I think the real issue here is my blame.
@asturur was right about the active selection ref on canvas. It has proven to be a wrong decision and this PR is an attempt to fix a side effect.
I think we should revert it and thus fix the underlying cause.
The need for the ref what for event subscription so if the canvas fires all the events of the active selection that should do.

@ShaMan123
Copy link
Contributor

Also we said we do not want fabric to change an object in unexpected ways.
The dev will be surprised or confused in some cases when this logic will take effect.

@ShaMan123
Copy link
Contributor

ShaMan123 commented Jan 9, 2024

#9561 is merged => closing this and will remove shouldResetTransform concept.
The dev should do that in onAfterLayout
Thanks for pointing this out, it made us fix my bad design.

@ShaMan123 ShaMan123 closed this Jan 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants